一覧に戻る

SveltekitのAPI routesでHonoを利用する

#TypeScript#Svelte#SvelteKit#Hono

はじめに

SveltekitはSvelteをベースとしたフロントエンドフレームワークです。Svelte自体の書き味などは言及するまでもなく、kitについて述べると、これはよくできたフレームワークで、ファイルベースルーティングやサーバーサイドレンダリング(SSR)など、モダンなフロントエンド周りの技術が、よい使用感で利用できます。

https://kit.svelte.dev/

よい使用感 is 何

何が「よい使用感」なのかを述べると、主観ですが、フレームワークというものは「お作法」を覚えなければならないですが、頭を悩ますようなお作法があまりないと思います。たとえば「アダプター」という仕組みは便利で、static exportしたければ専用のアダプターを利用するし、Cloudflare Pagesでホストするならそのためのアダプター、Node.jsなら…と、実装側はインフラの実体をあまり気にする必要がないのは良いと思います(もちろん、多少は意識する必要がある)。あとは、SSRとそれ以外を区別する際、コード側に特殊な記述("use client"みたいな)は必要ないとか、とにかく「it's for me」という感触があります。

API routes

API routesというのはたぶんNext.jsの用語ですが、まあ同じようなことが出来ます。つまりサーバーサイドの処理をSveltekit内で実装することが出来ます。例えばーー

// src/routes/api/hello/+server.ts
export const GET: RequestHandler = async ({ request }) => {
    // some logic
    return new Response('hello');
};

+server.tsというファイル名がSveltekitでは特別な意味を持ち、サーバーサイドの処理を記述するファイルを意味します。このファイルでGETという関数をexportすれば、ファイルベースルーティングにより定まるエンドポイントでGETリクエストを処理出来ます(この場合/api/hello)。

RequestHandlerについて

Requestや動作するインフラの環境変数などを取得するplatformを引数として、Responseを返却する関数です。結局のところRequestを受けResponseを返すということ。

SveltekitでHonoを使いたい

https://hono.dev/

Requestを受けResponseを返すというのはHonoと相性が良さそうだなぁと思いました。Honoでルーティングとか出来たらSveltekitが最強になるなぁと思いました。そう思って調べると先行事例がありました。なので以降、本記事はほぼ以下の記事の内容をなぞるの内容となります。

https://dev.to/bop/using-hono-with-sveltekit-full-type-safety-with-rpc-2h7

// src/routes/api/[...paths]/app.ts
import { Hono } from 'hono';

// 若干奇妙な書き方に見えるが、こう書いておくとRPCが利用出来て嬉しい(後述)
const router = new Hono()
	.get('/hello', async (c) => {
		return c.text('GET: hello!!');
	})
	.post('/hello', async (c) => {
		return c.text('POST: hello!!');
	})
	.delete('/hello', async (c) => {
		return c.text('DELETE: hello!!');
	});

export const app = new Hono().route('/api', router);
// src/routes/api/[...paths]/+server.ts
import type { RequestHandler } from '@sveltejs/kit';
import { hono } from './hono';

// 利用するメソッドを全て定義しておく
export const GET: RequestHandler = ({ request }) => hono.fetch(request);
export const POST: RequestHandler = ({ request }) => hono.fetch(request);
export const DELETE: RequestHandler = ({ request }) => hono.fetch(request);

この実装により、Honoの実装でHTTPリクエストを処理できるようになります。

curl http://localhost:5173/api/hello # hello
curl -X POST http://localhost:5173/api/hello # POST: hello!!
curl -X DELETE http://localhost:5173/api/hello # DELETE: hello!!

うれしい!

Hono RPC

HonoにはRPC(リモートプロシージャコール)を可能とする機能が含まれています。簡単に言い換えると、サーバー側を実装すると、同時にクライアント側の実装が出来上がるようなものです。TypeScriptなので、サーバー側のデータ型も良い感じにクライアント側で利用出来るのが強力です。

// src/routes/api/[...paths]/app.ts
import { Hono } from 'hono';
import { hc } from 'hono/client';

const router = new Hono()
	.get('/hello', async (c) => {
		return c.text('GET: hello!!');
	})
	.post('/hello', async (c) => {
		return c.text('POST: hello!!');
	})
	.delete('/hello', async (c) => {
		return c.text('DELETE: hello!!');
	});

export const app = new Hono().route('/api', router);

// 以下追記

type Router = typeof router;
let browserClient: ReturnType<typeof hc<Router>>;

export const makeClient = (fetch: Window['fetch']) => {
	const isBrowser = typeof window !== 'undefined';
	const origin = isBrowser ? window.location.origin : '';

	if (isBrowser && browserClient) {
		return browserClient;
	}

	const client = hc<Router>(origin + '/api', { fetch });

	if (isBrowser) {
		browserClient = client;
	}

	return client;
};

クライアント側から必要なのはmakeClientです。以下のように利用できます。

// src/routes/somewhere/+page.svelte
import { makeClient } from '../api/[...paths]/app.js';
// 略
onMount(async () => {
    const client = makeClient(fetch);

    const _get = await client.hello.$get();
    console.log(_get);

    const _post = await client.hello.$post();
    console.log(_post);
});
// 略

重要なことは、client.hello.$get()に型がついていることです。

ただしパラメータやリクエストボディまではカバー出来ていなかった。zodを入れると違うのかもしれない。

終わりに

Svelteの書き味はすばらしく、同じく素晴らしい書き味のHonoと組み合わせて、開発体験のよいフルスタックアプリケーションを作れそうですね。